import { ASTUtils, Selectors, toPattern } from '@angular-eslint/utils';
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
import { createESLintRule } from '../utils/create-eslint-rule';

export type Options = [];

export type MessageIds = 'preferInject';
export const RULE_NAME = 'prefer-inject';

export default createESLintRule<Options, MessageIds>({
  name: RULE_NAME,
  meta: {
    type: 'suggestion',
    docs: {
      description:
        'Prefer using the inject() function over constructor parameter injection',
      recommended: 'recommended',
    },
    schema: [],
    messages: {
      preferInject:
        "Prefer using the inject() function over constructor parameter injection. Use Angular's migration schematic to automatically refactor: ng generate @angular/core:inject",
    },
  },
  defaultOptions: [],
  create(context) {
    const angularDecoratorsPattern = toPattern([
      'Component',
      'Directive',
      'Injectable',
      'Pipe',
    ]);

    function shouldReportParameter(param: TSESTree.Parameter): boolean {
      let actualParam = param;
      let hasModifier = false;

      if (param.type === AST_NODE_TYPES.TSParameterProperty) {
        actualParam = param.parameter;
        hasModifier = true;
      }

      const decorators = (
        (param.type === AST_NODE_TYPES.TSParameterProperty
          ? param.parameter
          : param) as TSESTree.Parameter
      ).decorators;
      if (
        decorators?.some((d) => {
          const name = ASTUtils.getDecoratorName(d);
          return (
            name === 'Inject' ||
            name === 'Optional' ||
            name === 'Self' ||
            name === 'SkipSelf' ||
            name === 'Host'
          );
        })
      ) {
        return true;
      }

      if (hasModifier) {
        return true;
      }

      const typeAnnotation = (
        actualParam as TSESTree.Identifier | TSESTree.AssignmentPattern
      ).typeAnnotation;
      if (typeAnnotation) {
        switch (typeAnnotation.typeAnnotation.type) {
          case AST_NODE_TYPES.TSStringKeyword:
          case AST_NODE_TYPES.TSNumberKeyword:
          case AST_NODE_TYPES.TSBooleanKeyword:
          case AST_NODE_TYPES.TSBigIntKeyword:
          case AST_NODE_TYPES.TSSymbolKeyword:
          case AST_NODE_TYPES.TSAnyKeyword:
          case AST_NODE_TYPES.TSUnknownKeyword:
            return false;
          default:
            return true;
        }
      }

      return false;
    }

    return {
      [`${Selectors.decoratorDefinition(
        angularDecoratorsPattern,
      )} > ClassBody > MethodDefinition[kind="constructor"]`](
        node: TSESTree.MethodDefinition & {
          parent: TSESTree.ClassBody & { parent: TSESTree.ClassDeclaration };
        },
      ) {
        const params = (node.value as TSESTree.FunctionExpression).params ?? [];
        if (params.length === 0) {
          return;
        }
        for (const param of params) {
          if (shouldReportParameter(param)) {
            context.report({ node: param, messageId: 'preferInject' });
          }
        }
      },
    };
  },
});

export const RULE_DOCS_EXTENSION = {
  rationale:
    "The inject() function is Angular's modern dependency injection API that offers several advantages over constructor-based injection. First, it enables dependency injection outside of constructors, allowing you to use DI in functions, factories, and even at the class field level. This makes code more flexible and composable. Second, inject() is more concise and reduces boilerplate - you don't need constructor parameter properties or manual field assignments. Third, inject() works naturally with modern TypeScript features and tree-shaking. Fourth, it's required for using many modern Angular features like functional guards, interceptors, and the new signal-based APIs. Angular provides an automated migration schematic (ng generate @angular/core:inject) to convert constructor injection to inject(), making adoption straightforward. This is the recommended approach for all new Angular code.",
};
